package org.song.aspectJ;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.song.cache.DistributedLockCache;
import org.song.util.UUIDUtil;
import org.song.util.email.EmailUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.Objects;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;

/**
 * 分布式锁切面处理
 * 分布式锁作用？
 *   传统的单机服务器上共享资源有线程安全问题时可以使用 悲观锁 （排他锁）synchronized牺牲效率保证共享变量的安全性或者使用ThreadLocal牺牲内存保证共享变量的安全性
 *   系统分布式部署之后 各个微服务模块部署到不同的服务器上，集群部署之后各个服务器资源之间不具有可见性并发执行可能出现问题。比如电商系统的订单模块 用户A（线程A），用户B（线程B）
 *   同时在自己的购物车模块对同一间商品进行下单操作 购物车微服务调用订单微服务对库存进行操作，由于订单微服务是集群部署经过负载均衡用户A的下单请求打到订单模块1，用户B线程打到订单微服务模块2
 *   此时由于订单模块1和订单模块2都有库存信息此时订单模块1修改库存1而订单模块2的库存还是原库存此时订单模块2再次修改库存可能回出现库存问题。
 *   此时由于是集群部部署单机的JVM synchronized和ThreadLocal无法满足变共享资源的安全，引出分布式锁来解决分布式架构中共享资源的安全性，保证指定方法在任何一个时间段只能有一个进程一个线程执行
 *   一个好的分布式锁需要满足以下几点：
 *      1、可见性 （集群部署服务器共享分布式锁）
 *      2、互斥性（排他性）  （任何一个时间段只能有一个服务端端拿到分布式锁）
 *      3、防止死锁（一个服务端加锁之后需要及时释放分布式锁防止服务器宕机之后分布式锁无法释放）
 *      4、可重复入 （当前线程拿到分布式锁之后，事务提交之后可再次拿到分布式锁）
 *
 *    实现分布式锁的方式：
 *      1、数据库
 *      2、redis
 *      3、zk
 *
 *   数据库：
 *      数据库实现分布式锁有三种方式：
 *          1、自定义数据库分布式锁表+唯一索引
 *          2、对需要修改的数据加悲观锁（排他锁）行级锁
 *          3、对需要保证安全的表加乐观锁字段version CAS（compareAndSet）
 *
 *    redis：
 *        使用AOP横切注解+redis实现方法分布式锁
 *        前置增强拿锁 setnx expire设置过期时间
 *        方法执行结束之后 delete删除分布式锁
 *
 *    ZK: 基于ZK的目录节点来实现分布式锁
 *
 *    模拟redission实现分布式锁！
 *
 */
@Slf4j
@Aspect // 声明为切面类
@Component( value = "distributedLockAspect")  // 注入spring 容器 ApplicationContext
public class DistributedLockAspect {

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    // 分布式锁失效时间
    private static final Long LOCK_EXPIRE_TIME = 10000L;

    @Autowired( required = true )
    @Qualifier( value = "distributedLockCacheImpl")
    private DistributedLockCache distributedLockCache;

    @Autowired( required = true )
    @Qualifier( value = "emailUtil")
    private EmailUtil emailUtil;

    // 设置分布式锁注解横切关注点
    @Pointcut(value = "@annotation(org.song.annotation.GetDistributedLock)")
    public void distributedLockPointCut() {

    }

    @Around( value = "distributedLockPointCut()")
    public Object distributedLockAspect( ProceedingJoinPoint proceedingJoinPoint) {
        // 锁名为当前禁止并发的方法名 保证 互斥性 (获取目标对象)
        final MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        final String currentMethodName = signature.getMethod().getName();
        // 当前线程Id + UUID 作为分布式锁的value值 防止锁被别的客户端解除
        final String lockValue = Thread.currentThread().getId() + UUIDUtil.getUUIDString();
        Object targetMethodReturnValue = null;
        try {
            // 自旋加锁;
            while ( !distributedLockCache.tryToGetDistributedLock( currentMethodName , lockValue , LOCK_EXPIRE_TIME ) ) {
                log.info("方法：" + currentMethodName+ "，线程Id：" + Thread.currentThread().getId() + "加锁失败，自旋加锁。" );
            }
            log.info("方法：" + currentMethodName+ "，线程Id：" + Thread.currentThread().getId() + "加锁成功。" );
            // 重点！！！ 分布所锁高可用 加锁成功之后为每一个加锁线程起一个守护线程周期任务 周期判断当前线程是否锁被占有 YES 表示业务逻辑在执行中对锁进行延时即可
            //  this.delayDistributedLock( currentMethodName , lockValue );
            // 加锁成功执行业务逻辑
            targetMethodReturnValue = proceedingJoinPoint.proceed();
        } catch (Throwable throwable) {
            log.error("方法：" + currentMethodName+ "，线程Id：" + Thread.currentThread().getId() + "目标方法执行发生异常\n" + throwable.getMessage() );
        } finally {
            // lua脚本删除分布式锁
            if ( distributedLockCache.deleteDistributedLockByLua( currentMethodName , lockValue ) ) {
                log.info( "方法：" + currentMethodName+ "，线程Id：" + Thread.currentThread().getId() + "执行目标方法结束。lua脚本手动删除分布式锁成功！" );
            }
        }
        return targetMethodReturnValue;
    }

    /**
     * 为分布所实现延时 实现高可用
     * @param lockName
     * @param lockValue
     */
   // 每 过期时间的1/3秒判断一次 FIXME: 启用周期任务为每个抢到锁的线程延时，存在业务执行完毕周期任务还在为锁延时出现死锁！！！
    public void delayDistributedLock( String lockName , String lockValue ) {
        new Timer( true ).schedule(new TimerTask() {
            @Override
            public void run() {
                log.info("执行延时方法");
                // 判当是否是当前线程
                if ( Objects.equals( lockValue , redisTemplate.opsForValue().get( lockName ) ) ) {
                    // 当前线程为加锁线程直接重置锁时间 ，直到目标方法执行完毕不在持有锁自动释放（过期策略）
                    redisTemplate.opsForValue().set( lockName , lockValue , LOCK_EXPIRE_TIME , TimeUnit.MILLISECONDS);
                }
            }
        } , new Date() , LOCK_EXPIRE_TIME/3 );
    }




}
